"""
Paddle 客户端

提供 PaddleOCR-VL 和印章识别服务的完整封装
支持与 MinerU 相同的接口，确保上下游兼容
"""

import asyncio
import base64
import json
import re
import time
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, Union, List, Tuple
from io import BytesIO

import fitz
import httpx
from loguru import logger

from config.paddle_config import (
    get_paddle_config,
    get_api_endpoint,
    CURRENT_ENV
)
from src.services.paddle.models import (
    PaddleOCRVLResponse,
    PaddleSealRecognitionResponse,
    PaddleParseResult,
    PaddlePage,
    PaddleContent,
    PaddleLayoutBlock,
    PaddleLine,
    PaddleSpan,
    MarkdownResult,
    SealDetection,
    create_bbox_from_list
)


class PaddleError(Exception):
    """Paddle服务错误"""
    pass


class PaddleClient:
    """
    Paddle API客户端
    
    整合 PaddleOCR-VL 和印章识别两个服务
    提供与 MinerU 兼容的接口
    """
    
    def __init__(self, env: str = None):
        """
        初始化Paddle客户端
        
        Args:
            env: 环境名称（dev/prod），默认使用配置文件中的环境
        """
        self.env = env or CURRENT_ENV
        self.config = get_paddle_config(self.env)
        
        # API配置
        self.ocr_vl_config = self.config['api']['ocr_vl']
        self.seal_config = self.config['api']['seal_recognition']
        
        # 解析选项
        self.ocr_vl_options = self.config['ocr_vl_options']
        self.seal_recognition_options = self.config['seal_recognition_options']
        self.seal_extract_config = self.config['seal_config']
        
        # 错误处理配置
        self.error_config = self.config['error']
        self.debug_config = self.config['debug']
        
        # 创建HTTP客户端
        self.client = httpx.AsyncClient(
            timeout=httpx.Timeout(self.ocr_vl_config['timeout']),
            follow_redirects=True,
            verify=False  # 禁用SSL验证（测试环境）
        )
        
        logger.debug(
            f"Paddle客户端初始化 | 环境: {self.env}"
        )
    
    async def close(self):
        """关闭客户端"""
        await self.client.aclose()
    
    async def __aenter__(self):
        """异步上下文管理器入口"""
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """异步上下文管理器出口"""
        await self.close()
    
    async def parse_file(
        self,
        file_path: Optional[Union[str, Path]] = None,
        file_content: Optional[bytes] = None,
        file_name: Optional[str] = None,
        options: Optional[Dict[str, Any]] = None
    ) -> PaddleParseResult:
        """
        解析文档文件（与 MinerU 兼容的接口）
        
        处理流程：
        1. 调用 PaddleOCR-VL 进行版面解析
        2. 提取所有印章（base64）
        3. 调用印章识别服务识别印章文本
        4. 将印章文本合并到主文本中
        5. 返回与 MinerU 兼容的解析结果
        
        Args:
            file_path: 文件路径或URL
            file_content: 文件内容（字节）
            file_name: 文件名称
            options: 解析选项
            
        Returns:
            PaddleParseResult: 解析结果（兼容 MinerU）
            
        Raises:
            PaddleError: 解析失败时抛出
        """
        start_time = time.time()
        
        try:
            # 准备文件内容
            if file_path:
                file_path_str = str(file_path)
                
                # 检查是否为 URL
                if file_path_str.startswith(('http://', 'https://')):
                    # 从 URL 下载文件
                    logger.info(f"从 URL 下载文件: {file_path_str}")
                    response = await self.client.get(file_path_str)
                    response.raise_for_status()
                    file_content = response.content
                    
                    # 从 URL 中提取文件名
                    if not file_name:
                        from urllib.parse import urlparse, unquote
                        parsed_url = urlparse(file_path_str)
                        file_name = unquote(Path(parsed_url.path).name)
                else:
                    # 本地文件路径
                    file_path = Path(file_path)
                    file_name = file_name or file_path.name
                    with open(file_path, 'rb') as f:
                        file_content = f.read()
            elif file_content is None:
                raise ValueError("必须提供 file_path 或 file_content")
            
            if not file_name:
                file_name = 'unknown_file'
            
            # 获取文件类型
            file_type = Path(file_name).suffix.lower()
            pdf_page_sizes: Optional[List[Tuple[float, float]]] = None
            if file_type == '.pdf':
                pdf_page_sizes = self._get_pdf_page_sizes(file_content)
            
            # 1. 调用 PaddleOCR-VL 进行版面解析
            ocr_vl_result = await self._call_ocr_vl(file_content, file_name, options)
            
            # 2. 提取印章并识别文本
            seal_texts_by_page = {}
            if self.seal_extract_config['enabled']:
                seal_texts_by_page = await self._extract_seal_texts(
                    ocr_vl_result, 
                    file_content, 
                    file_name
                )
            
            # 3. 转换为兼容 MinerU 的格式
            parse_result = self._convert_to_parse_result(
                ocr_vl_result,
                seal_texts_by_page,
                file_name,
                file_type,
                pdf_page_sizes=pdf_page_sizes
            )
            
            # 记录解析时间
            parse_time = time.time() - start_time
            parse_result.parse_time = parse_time
            
            if not parse_result.success:
                logger.error(f"Paddle解析失败 | {file_name}: {parse_result.error_message}")
            
            return parse_result
            
        except httpx.HTTPError as e:
            logger.error(f"Paddle API调用失败: {e}")
            raise PaddleError(f"API调用失败: {str(e)}")
        except ValueError as e:
            # ValueError直接抛出（不包装）
            raise
        except Exception as e:
            logger.error(f"文件解析异常: {e}", exc_info=True)
            raise PaddleError(f"解析失败: {str(e)}")
    
    async def _call_ocr_vl(
        self,
        file_content: bytes,
        file_name: str,
        options: Optional[Dict[str, Any]] = None
    ) -> PaddleOCRVLResponse:
        """
        调用 PaddleOCR-VL API
        
        Args:
            file_content: 文件内容
            file_name: 文件名
            options: 解析选项
            
        Returns:
            PaddleOCRVLResponse: API响应结果
        """
        url = get_api_endpoint('ocr_vl', self.env)
        
        # 将文件内容转换为 base64
        file_base64 = base64.b64encode(file_content).decode('utf-8')
        
        # 合并解析选项
        parse_opts = {**self.ocr_vl_options, **(options or {})}
        
        # 根据文件扩展名自动设置 fileType（如果未指定）
        if 'fileType' not in parse_opts or parse_opts['fileType'] is None:
            file_ext = Path(file_name).suffix.lower()
            if file_ext == '.pdf':
                parse_opts['fileType'] = 0  # PDF 文件
            elif file_ext in ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp']:
                parse_opts['fileType'] = 1  # 图像文件
            else:
                # 默认按 PDF 处理
                parse_opts['fileType'] = 0
        
        # 准备请求体
        request_body = {
            'file': file_base64,
            **{k: v for k, v in parse_opts.items() if v is not None}
        }
        
        # 重试逻辑
        max_retries = self.ocr_vl_config['max_retries']
        retry_delay = self.ocr_vl_config['retry_delay']
        
        last_error = None
        for attempt in range(max_retries):
            try:
                
                response = await self.client.post(
                    url,
                    json=request_body,
                    timeout=self.ocr_vl_config['timeout']
                )
                
                # 检查响应状态
                if response.status_code == 200:
                    try:
                        result = response.json()
                    except json.JSONDecodeError as e:
                        error_msg = f"JSON解析失败: {e}, 响应内容: {response.text[:200]}"
                        logger.error(error_msg)
                        raise PaddleError(error_msg)
                    
                    self._save_debug_payload('paddle_ocr_vl', result)
                    if self.debug_config.get('verbose'):
                        logger.debug(f"OCR-VL 原始响应: {json.dumps(result, ensure_ascii=False)[:2000]}")

                    # 解析响应
                    return self._parse_ocr_vl_response(result)
                else:
                    # 尝试解析错误响应
                    try:
                        error_result = response.json()
                        error_msg = f"API返回错误: {error_result.get('errorMsg', 'Unknown error')} (状态码: {response.status_code})"
                    except:
                        error_msg = f"API返回错误状态码: {response.status_code}, 响应: {response.text[:200]}"
                    
                    self._save_debug_payload('paddle_ocr_vl_error', response.text, suffix='txt')
                    
                    # 检查是否可重试
                    if response.status_code in self.error_config['retryable_status_codes']:
                        last_error = PaddleError(error_msg)
                        if attempt < max_retries - 1:
                            logger.debug(f"OCR-VL重试 ({attempt + 2}/{max_retries})")
                            await asyncio.sleep(retry_delay)
                            continue
                    
                    logger.warning(error_msg)
                    raise PaddleError(error_msg)
                    
            except httpx.TimeoutException as e:
                last_error = PaddleError(f"请求超时: {str(e)}")
                if attempt < max_retries - 1:
                    logger.debug(f"OCR-VL超时重试 ({attempt + 2}/{max_retries})")
                    await asyncio.sleep(retry_delay)
                    continue
                logger.warning(f"OCR-VL请求超时: {e}")
                    
            except httpx.RequestError as e:
                last_error = PaddleError(f"请求错误: {str(e)}")
                if attempt < max_retries - 1:
                    logger.debug(f"OCR-VL请求错误重试 ({attempt + 2}/{max_retries})")
                    await asyncio.sleep(retry_delay)
                    continue
                logger.warning(f"OCR-VL请求错误: {e}")
        
        # 所有重试都失败了
        raise last_error or PaddleError("API调用失败")
    
    def _parse_ocr_vl_response(self, result: Dict[str, Any]) -> PaddleOCRVLResponse:
        """
        解析 PaddleOCR-VL 响应
        
        Args:
            result: API响应JSON
            
        Returns:
            PaddleOCRVLResponse: 解析后的响应对象
        """
        try:
            log_id = result.get('logId', '')
            error_code = result.get('errorCode', -1)
            error_msg = result.get('errorMsg', 'Unknown error')
            
            if error_code != 0:
                raise PaddleError(f"OCR-VL 解析失败: {error_msg}")
            
            result_data = result.get('result', {})
            layout_parsing_results = result_data.get('layoutParsingResults', [])
            data_info = result_data.get('dataInfo', {})
            
            # 解析每页的结果
            parsed_results = []
            for page_result in layout_parsing_results:
                pruned_result = page_result.get('prunedResult', {})
                markdown_data = page_result.get('markdown', {})
                output_images = page_result.get('outputImages')
                input_image = page_result.get('inputImage')
                
                # 解析 Markdown 结果
                markdown_text_raw = markdown_data.get('text', '')
                logger.debug(f"Paddle API 返回页面 markdown.text 长度: {len(markdown_text_raw)} 字符")
                
                markdown = MarkdownResult(
                    text=markdown_text_raw,
                    images=markdown_data.get('images', {}),
                    is_start=markdown_data.get('isStart', True),
                    is_end=markdown_data.get('isEnd', True)
                )
                
                from src.services.paddle.models import PaddleLayoutParsingResult
                parsed_results.append(PaddleLayoutParsingResult(
                    pruned_result=pruned_result,
                    markdown=markdown,
                    output_images=output_images,
                    input_image=input_image
                ))
            
            return PaddleOCRVLResponse(
                log_id=log_id,
                error_code=error_code,
                error_msg=error_msg,
                layout_parsing_results=parsed_results,
                data_info=data_info
            )
            
        except Exception as e:
            logger.error(f"解析 OCR-VL 响应失败: {e}", exc_info=True)
            raise PaddleError(f"响应解析失败: {str(e)}")
    
    @staticmethod
    def _normalize_block_type(raw_type: Optional[str]) -> str:
        """
        规范化 Paddle 返回的 block 类型，映射到统一的内容类型
        """
        if not raw_type:
            return 'text'

        raw_type = raw_type.lower()
        type_mapping = {
            'paragraph_title': 'text',
            'doc_title': 'text',
            'title': 'text',
            'caption': 'text',
            'header': 'text',
            'footer': 'text',
            'paragraph': 'text',
            'plain': 'text',
            'textline': 'text',
            'seal': 'seal',
            'table': 'table',
            'table_caption': 'text',
            'chart': 'image',
            'figure': 'image',
            'image': 'image',
            'header_image': 'image',
            'equation': 'formula',
            'formula': 'formula',
            'code': 'code',
            'list': 'list',
            'bulleted_list': 'list',
            'numbered_list': 'list',
        }
        return type_mapping.get(raw_type, raw_type)

    @staticmethod
    def _extract_image_key_from_block_content(block_content: Optional[str]) -> Optional[str]:
        """
        从 block_content 中提取图片的相对路径（src 属性）
        """
        if not block_content:
            return None

        match = re.search(r'src=["\']([^"\']+)["\']', block_content)
        if match:
            return match.group(1)
        return None

    @staticmethod
    def _strip_block_text(block_content: Optional[str]) -> str:
        """
        去除 block_content 中的 HTML 标签，提取纯文本
        """
        if not block_content:
            return ''

        # 去除 img 标签
        content = re.sub(r'<img[^>]*>', '', block_content)
        # 去除其他 HTML 标签
        content = re.sub(r'<[^>]+>', '', content)
        # 合并空白
        content = re.sub(r'\s+', ' ', content)
        return content.strip()

    @staticmethod
    def _is_same_bbox(bbox_a: Optional[List[float]], bbox_b: Optional[List[float]], tolerance: float = 5.0) -> bool:
        """
        判断两个 bbox 是否可以视为相同
        """
        if not bbox_a or not bbox_b:
            return False
        if len(bbox_a) != 4 or len(bbox_b) != 4:
            return False
        return all(abs(a - b) <= tolerance for a, b in zip(bbox_a, bbox_b))
    
    def _match_score_from_layout_det(
        self,
        block_bbox: List[float],
        layout_det_boxes: List[Dict[str, Any]],
        block_type: str
    ) -> Optional[float]:
        """
        从 layout_det_res.boxes 中匹配 score
        
        通过 bbox 匹配找到对应的检测框，提取其 score
        
        Args:
            block_bbox: block 的 bbox（像素坐标，未转换）
            layout_det_boxes: layout_det_res.boxes 列表
            block_type: block 类型
            
        Returns:
            Optional[float]: 匹配到的 score，未匹配返回 None
        """
        if not block_bbox or not layout_det_boxes:
            return None
        
        # 遍历 layout_det_res.boxes，找到最匹配的
        best_match_score = None
        best_match_iou = 0.0
        
        for det_box in layout_det_boxes:
            # 获取检测框的坐标
            det_coordinate = det_box.get('coordinate')
            if not det_coordinate or len(det_coordinate) < 4:
                continue
            
            # 标准化坐标格式（可能是4点或8点）
            try:
                det_bbox = create_bbox_from_list(det_coordinate)
            except:
                continue
            
            # 计算 IoU（交并比）
            iou = self._calculate_bbox_iou(block_bbox, det_bbox)
            
            # 如果 IoU > 0.5，认为是匹配的
            if iou > 0.5 and iou > best_match_iou:
                best_match_iou = iou
                best_match_score = det_box.get('score')
                
                # 如果 IoU > 0.9，直接返回（完全匹配）
                if iou > 0.9:
                    return best_match_score
        
            return best_match_score
    
    @staticmethod
    def _calculate_bbox_iou(bbox_a: List[float], bbox_b: List[float]) -> float:
        """
        计算两个 bbox 的 IoU（交并比）
        
        Args:
            bbox_a: bbox [x0, y0, x1, y1]
            bbox_b: bbox [x0, y0, x1, y1]
            
        Returns:
            float: IoU 值（0-1）
        """
        # 计算交集
        x0_inter = max(bbox_a[0], bbox_b[0])
        y0_inter = max(bbox_a[1], bbox_b[1])
        x1_inter = min(bbox_a[2], bbox_b[2])
        y1_inter = min(bbox_a[3], bbox_b[3])
        
        # 如果没有交集
        if x0_inter >= x1_inter or y0_inter >= y1_inter:
            return 0.0
        
        # 交集面积
        inter_area = (x1_inter - x0_inter) * (y1_inter - y0_inter)
        
        # 两个 bbox 的面积
        area_a = (bbox_a[2] - bbox_a[0]) * (bbox_a[3] - bbox_a[1])
        area_b = (bbox_b[2] - bbox_b[0]) * (bbox_b[3] - bbox_b[1])
        
        # 并集面积
        union_area = area_a + area_b - inter_area
        
        # IoU
        if union_area == 0:
            return 0.0
        
        return inter_area / union_area

    def _save_debug_payload(self, service: str, payload: Any, suffix: str = 'json'):
        """
        按配置保存调试信息（如原始响应）
        """
        if not self.debug_config.get('enabled') or not self.debug_config.get('save_raw_response'):
            return

        output_dir = Path(self.debug_config.get('debug_output_dir', 'logs/paddle_debug'))
        try:
            output_dir.mkdir(parents=True, exist_ok=True)
        except Exception:
            return

        timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')
        file_path = output_dir / f"{service}_{timestamp}.{suffix}"

        try:
            if suffix == 'json':
                with file_path.open('w', encoding='utf-8') as fp:
                    json.dump(payload, fp, ensure_ascii=False, indent=2)
            else:
                with file_path.open('w', encoding='utf-8') as fp:
                    fp.write(str(payload))
        except Exception:
            pass

    async def _extract_seal_texts(
        self,
        ocr_vl_result: PaddleOCRVLResponse,
        file_content: bytes,
        file_name: str
    ) -> Dict[int, List[SealDetection]]:
        """
        提取并识别印章文本
        
        从 OCR-VL 结果中提取印章图像（base64），
        调用印章识别服务识别文本
        
        Args:
            ocr_vl_result: OCR-VL 解析结果
            file_content: 原始文件内容
            file_name: 文件名
            
        Returns:
            Dict[int, List[SealDetection]]: 每页的印章识别结果
        """
        seal_texts_by_page = {}
        
        try:
            # 遍历每页，查找印章
            for page_idx, page_result in enumerate(ocr_vl_result.layout_parsing_results):
                pruned_result = page_result.pruned_result
                markdown_images = page_result.markdown.images or {}

                raw_blocks = pruned_result.get('parsing_res_list')
                block_source = 'parsing_res_list'
                if not raw_blocks:
                    raw_blocks = pruned_result.get('layout_dets', [])
                    block_source = 'layout_dets'

                # 提取印章区域
                seal_images = []
                for det in raw_blocks or []:
                    raw_type = det.get('block_label') if block_source == 'parsing_res_list' else det.get('category_type')
                    block_type = self._normalize_block_type(raw_type)
                    if block_type != 'seal':
                        continue

                    bbox_list = det.get('block_bbox') if block_source == 'parsing_res_list' else det.get('bbox')
                    bbox: List[float] = []
                    if bbox_list:
                        try:
                            # 注意：此处bbox仍为Paddle的像素坐标，将在_convert_to_parse_result中统一转换
                            bbox = create_bbox_from_list(bbox_list)
                        except Exception:
                            pass

                    block_content = det.get('block_content') if block_source == 'parsing_res_list' else det.get('text')
                    image_base64 = det.get('image_base64') or det.get('image')
                    image_key = det.get('image_path')

                    if not image_base64:
                        if not image_key and block_source == 'parsing_res_list':
                            image_key = self._extract_image_key_from_block_content(block_content)
                        if image_key and markdown_images:
                            image_base64 = markdown_images.get(image_key)

                    if not image_base64 and page_result.output_images:
                        if image_key and page_result.output_images:
                            image_base64 = page_result.output_images.get(image_key)

                    if not image_base64 and page_result.input_image and block_type == 'seal':
                        # 部分产线可能直接返回单页图像
                        image_base64 = page_result.input_image

                    if image_base64:
                        seal_images.append({
                            'bbox': bbox,
                            'image_base64': image_base64,
                            'image_key': image_key
                        })

                # 如果有印章，调用印章识别服务
                if seal_images:
                    # 为每个印章调用识别服务
                    page_seals = []
                    for seal_image in seal_images:
                        try:
                            seal_recognition = await self._recognize_seal(
                                seal_image['image_base64'],
                                file_name
                            )
                            
                            if seal_recognition:
                                text = seal_recognition.get('text', '')
                                score = seal_recognition.get('score', 0.0)
                                page_seals.append(SealDetection(
                                    bbox=seal_image['bbox'],
                                    text=text,
                                    score=score,
                                    image_key=seal_image.get('image_key'),
                                    image_base64=seal_image['image_base64']
                                ))
                                logger.debug(f"印章识别: {text} (score={score:.2f})")
                        except Exception as e:
                            logger.debug(f"印章识别失败: {e}")
                            continue
                    
                    if page_seals:
                        seal_texts_by_page[page_idx] = page_seals
            
            return seal_texts_by_page
            
        except Exception as e:
            logger.error(f"提取印章文本失败: {e}", exc_info=True)
            return {}
    
    def _get_pdf_page_sizes(
        self,
        file_content: bytes
    ) -> Optional[List[Tuple[float, float]]]:
        """
        获取 PDF 每页的实际尺寸（单位：pt）
        """
        try:
            doc = fitz.open(stream=file_content, filetype="pdf")
            page_sizes: List[Tuple[float, float]] = []
            for page in doc:
                rect = page.rect
                page_sizes.append((rect.width, rect.height))
            doc.close()
            return page_sizes
        except Exception as e:
            logger.debug(f"无法获取 PDF 页面尺寸: {e}")
            return None
    
    async def _recognize_seal(
        self,
        seal_image_base64: str,
        file_name: str
    ) -> Optional[Dict[str, Any]]:
        """
        调用印章识别服务识别单个印章
        
        Args:
            seal_image_base64: 印章图像的 base64 编码
            file_name: 文件名（用于日志）
            
        Returns:
            Optional[Dict[str, Any]]: 识别结果，包含 text 与 score
        """
        url = get_api_endpoint('seal_recognition', self.env)
        
        # 准备请求体
        request_body = {
            'file': seal_image_base64,
            'fileType': 1,  # 图像
            **{k: v for k, v in self.seal_recognition_options.items() if v is not None}
        }
        
        try:
            response = await self.client.post(
                url,
                json=request_body,
                timeout=self.seal_config['timeout']
            )
            
            if response.status_code == 200:
                result = response.json()
                self._save_debug_payload('paddle_seal_recognition', result)
                
                # 解析响应
                if result.get('errorCode') == 0:
                    seal_results = result.get('result', {}).get('sealRecResults', [])
                    
                    if seal_results and len(seal_results) > 0:
                        # 提取印章文本
                        first_result = seal_results[0]
                        pruned_result = first_result.get('prunedResult', {})
                        
                        # 实际的印章识别结果在 seal_res_list 中
                        seal_res_list = pruned_result.get('seal_res_list', [])
                        
                        if seal_res_list:
                            # 取第一个印章识别结果
                            first_seal = seal_res_list[0]
                            
                            # rec_texts 包含识别出的文本列表
                            rec_texts = first_seal.get('rec_texts', [])
                            rec_scores = first_seal.get('rec_scores', [])
                            
                            if rec_texts:
                                # 合并所有识别出的文本
                                text = ''.join(rec_texts)
                                # 使用平均分数
                                score = sum(rec_scores) / len(rec_scores) if rec_scores else 0.0
                                
                                return {
                                    'text': text,
                                    'score': score
                                }
                            
                            # 检查是否检测到了文本区域但未能识别
                            dt_polys = first_seal.get('dt_polys', [])
                            if dt_polys:
                                logger.debug(f"印章检测到文本区域但识别失败")
                            else:
                                logger.debug("印章识别：未检测到文本区域")
                        
                        # 兼容其他可能的格式
                        seal_det_res = pruned_result.get('seal_det_res', {})
                        boxes = seal_det_res.get('boxes', [])
                        if boxes:
                            for box in boxes:
                                text = box.get('text', '')
                                if text:
                                    return {
                                        'text': text,
                                        'score': box.get('score', 0.0)
                                    }
                        
                        # 检查是否印章检测失败
                        logger.debug(f"印章识别失败: 图像质量不佳或参数需调整")
                
                logger.debug(f"印章识别未返回文本")
                return None
            else:
                logger.debug(f"印章识别API返回错误: {response.status_code}")
                self._save_debug_payload('paddle_seal_recognition_error', response.text, suffix='txt')
                return None
                
        except Exception as e:
            logger.error(f"调用印章识别服务失败: {e}")
            return None
    
    def _convert_to_parse_result(
        self,
        ocr_vl_result: PaddleOCRVLResponse,
        seal_texts_by_page: Dict[int, List[SealDetection]],
        file_name: str,
        file_type: str,
        pdf_page_sizes: Optional[List[Tuple[float, float]]] = None
    ) -> PaddleParseResult:
        """
        将 Paddle 响应转换为兼容 MinerU 的 ParseResult
        
        Args:
            ocr_vl_result: OCR-VL 解析结果
            seal_texts_by_page: 每页的印章识别结果
            file_name: 文件名
            file_type: 文件类型
            
        Returns:
            PaddleParseResult: 兼容 MinerU 的解析结果
        """
        try:
            pages: List[PaddlePage] = []
            all_markdown_texts: List[str] = []

            page_info_list = None
            if isinstance(ocr_vl_result.data_info, dict):
                page_info_list = ocr_vl_result.data_info.get('pages')

            for page_idx, page_result in enumerate(ocr_vl_result.layout_parsing_results):
                pruned_result = page_result.pruned_result or {}
                markdown_result = page_result.markdown
                markdown_images = markdown_result.images or {}

                # ===  简化的坐标处理策略 ===
                # Paddle 返回的是 144 DPI 渲染下的像素坐标
                # 我们直接保留原生像素坐标，不做转换
                # 配合专用的 PaddlePDFHighlighter (144 DPI) 使用，确保坐标完全对齐
                
                # 获取页面像素尺寸（用于页面尺寸记录）
                default_pixel_width = 1186  # A4 @144 DPI
                default_pixel_height = 1672
                
                if isinstance(page_info_list, list) and page_idx < len(page_info_list):
                    page_info = page_info_list[page_idx] or {}
                    page_pixel_width = page_info.get('width', default_pixel_width)
                    page_pixel_height = page_info.get('height', default_pixel_height)
                else:
                    page_info = pruned_result.get('page_info', {}) or {}
                    page_pixel_width = page_info.get('width', default_pixel_width)
                    page_pixel_height = page_info.get('height', default_pixel_height)
                
                # 页面尺寸直接使用像素尺寸（144 DPI）
                # 不再转换为点坐标，保持与 Paddle 原始数据一致
                page_width = page_pixel_width
                page_height = page_pixel_height
                
                logger.debug(
                    f"页面 {page_idx} | "
                    f"像素尺寸: {page_pixel_width}x{page_pixel_height} px @ 144 DPI | "
                    f"坐标系统: 原生像素坐标（无转换）"
                )

                if pruned_result.get('parsing_res_list'):
                    block_source = 'parsing_res_list'
                    raw_blocks = pruned_result.get('parsing_res_list', [])
                else:
                    block_source = 'layout_dets'
                    raw_blocks = pruned_result.get('layout_dets', [])

                # 提取 layout_det_res 中的 score 信息（用于匹配到 block）
                layout_det_boxes = []
                if pruned_result.get('layout_det_res'):
                    layout_det_boxes = pruned_result.get('layout_det_res', {}).get('boxes', [])
                
                contents: List[PaddleContent] = []
                layout_blocks: List[PaddleLayoutBlock] = []

                for raw_block in raw_blocks or []:
                    raw_type = raw_block.get('block_label') if block_source == 'parsing_res_list' else raw_block.get('category_type')
                    block_type = self._normalize_block_type(raw_type)

                    bbox_list = raw_block.get('block_bbox') if block_source == 'parsing_res_list' else raw_block.get('bbox')
                    bbox_value: Optional[List[float]] = None
                    if bbox_list:
                        try:
                            # 标准化bbox格式，保留原生像素坐标（无转换）
                            bbox_value = create_bbox_from_list(bbox_list)
                        except Exception:
                            pass

                    block_content = raw_block.get('block_content') if block_source == 'parsing_res_list' else raw_block.get('text')
                    text = ''
                    table_body = None
                    text_format = None
                    image_base64 = raw_block.get('image_base64') or raw_block.get('image')
                    img_path = raw_block.get('image_path')

                    if block_type == 'table':
                        table_body = raw_block.get('html') if block_source == 'layout_dets' else (block_content or '')
                        text = self._strip_block_text(block_content or '')
                    elif block_type == 'image':
                        if block_source == 'parsing_res_list' and not img_path:
                            img_path = self._extract_image_key_from_block_content(block_content)
                    elif block_type == 'seal':
                        if block_source == 'parsing_res_list' and not img_path:
                            img_path = self._extract_image_key_from_block_content(block_content)
                    elif block_type == 'formula':
                        text = raw_block.get('text') or raw_block.get('block_text') or self._strip_block_text(block_content or '')
                        text_format = 'latex'
                    elif block_type == 'code':
                        text = raw_block.get('block_text') or (block_content or '')
                    elif block_type == 'list':
                        text = raw_block.get('block_text') or self._strip_block_text(block_content or '')
                    else:
                        if block_source == 'parsing_res_list':
                            text = raw_block.get('block_text') or self._strip_block_text(block_content or '')
                        else:
                            text = raw_block.get('text') or ''

                    if not image_base64 and img_path and markdown_images:
                        image_base64 = markdown_images.get(img_path)

                    if not image_base64 and block_type in ('image', 'seal'):
                        fallback_key = self._extract_image_key_from_block_content(block_content)
                        if fallback_key:
                            if not img_path:
                                img_path = fallback_key
                            image_base64 = markdown_images.get(fallback_key)

                    # 尝试从 layout_det_res.boxes 中匹配 score
                    block_score = None
                    if bbox_value and layout_det_boxes:
                        block_score = self._match_score_from_layout_det(
                            bbox_value, 
                            layout_det_boxes,
                            block_type
                        )
                    
                    content = PaddleContent(
                        type=block_type,
                        text=text,
                        bbox=bbox_value,
                        page_idx=page_idx,
                        img_path=img_path,
                        image_base64=image_base64,
                        table_body=table_body,
                        text_format=text_format,
                        score=block_score  # 添加 score
                    )
                    contents.append(content)

                    # 构建 layout_block 用于溯源
                    # 确保即使 text 为空，也创建包含基本信息的 block
                    span_text = text or self._strip_block_text(block_content or '') or ''
                    
                    # 为表格类型，尝试从 table_body 提取文本
                    if block_type == 'table' and not span_text and table_body:
                        # 简单去除 HTML 标签获取纯文本（用于溯源）
                        import re
                        span_text = re.sub(r'<[^>]+>', ' ', table_body).strip()
                    
                    # 创建 spans 和 lines
                    spans = []
                    if span_text:
                        # 将文本按换行符分割为多个 span（提高溯源精度）
                        text_parts = span_text.split('\n')
                        for part in text_parts:
                            part = part.strip()
                            if part:  # 只添加非空文本
                                spans.append(PaddleSpan(
                                    bbox=bbox_value or [],
                                    text=part
                                ))
                    
                    # 即使没有文本，也创建一个空的 layout_block（保持结构完整性）
                    layout_blocks.append(PaddleLayoutBlock(
                        bbox=bbox_value or [],
                        lines=[PaddleLine(bbox=bbox_value or [], spans=spans)] if spans else [],
                        category_type=block_type
                    ))

                # 印章坐标保留原生像素坐标（无转换）
                recognized_seals_raw = seal_texts_by_page.get(page_idx, [])
                recognized_seals = []
                for seal in recognized_seals_raw:
                    # 直接使用原始bbox，不做转换
                    recognized_seals.append(SealDetection(
                        bbox=seal.bbox or [],
                        text=seal.text,
                        score=seal.score,
                        image_key=seal.image_key,
                        image_base64=seal.image_base64
                    ))
                remaining_seals = list(recognized_seals)

                for content in contents:
                    if content.type != 'seal':
                        continue

                    matched_seal = None
                    if content.img_path:
                        matched_seal = next((seal for seal in remaining_seals if seal.image_key == content.img_path), None)
                    if not matched_seal and content.image_base64:
                        matched_seal = next((seal for seal in remaining_seals if seal.image_base64 == content.image_base64), None)
                    if not matched_seal and content.bbox:
                        matched_seal = next((seal for seal in remaining_seals if self._is_same_bbox(seal.bbox, content.bbox)), None)

                    if matched_seal:
                        formatted_text = self.seal_extract_config['seal_text_format'].format(text=matched_seal.text)
                        content.text = formatted_text
                        content.score = matched_seal.score
                        if matched_seal.image_base64 and not content.image_base64:
                            content.image_base64 = matched_seal.image_base64
                        remaining_seals.remove(matched_seal)

                for seal_det in remaining_seals:
                    formatted_text = self.seal_extract_config['seal_text_format'].format(text=seal_det.text)
                    seal_bbox = seal_det.bbox if seal_det.bbox else []
                    contents.append(PaddleContent(
                        type='seal',
                        text=formatted_text,
                        bbox=seal_det.bbox if seal_det.bbox else None,
                        page_idx=page_idx,
                        img_path=seal_det.image_key,
                        image_base64=seal_det.image_base64,
                        score=seal_det.score
                    ))
                    spans = [PaddleSpan(bbox=seal_bbox, text=formatted_text)]
                    layout_blocks.append(PaddleLayoutBlock(
                        bbox=seal_bbox,
                        lines=[PaddleLine(bbox=seal_bbox, spans=spans)],
                        category_type='seal'
                    ))

                page = PaddlePage(
                    page_num=page_idx,
                    width=page_width,
                    height=page_height,
                    contents=contents,
                    layout_blocks=layout_blocks
                )
                pages.append(page)

                markdown_text = markdown_result.text or ''
                
                if not markdown_text:
                    logger.warning(f"页面 {page_idx} markdown_text 为空！")
                
                if self.seal_extract_config['merge_seal_text'] and seal_texts_by_page.get(page_idx):
                    seal_lines = [
                        self.seal_extract_config['seal_text_format'].format(text=seal.text)
                        for seal in seal_texts_by_page[page_idx]
                        if seal.text
                    ]
                    if seal_lines:
                        markdown_text = markdown_text.rstrip() + '\n\n' + '\n\n'.join(seal_lines)

                all_markdown_texts.append(markdown_text)

            md_content = '\n\n---\n\n'.join(all_markdown_texts)
            logger.info(f"Paddle 解析完成，md_content 总长度: {len(md_content)} 字符，来自 {len(all_markdown_texts)} 页")

            parse_result = PaddleParseResult(
                success=True,
                file_name=file_name,
                file_type=file_type,
                total_pages=len(pages),
                backend='paddle',
                pages=pages,
                md_content=md_content,
                metadata={
                    'ocr_vl_log_id': ocr_vl_result.log_id,
                    'seal_count': sum(len(seals) for seals in seal_texts_by_page.values())
                }
            )

            return parse_result
            
        except Exception as e:
            logger.error(f"转换解析结果失败: {e}", exc_info=True)
            return PaddleParseResult(
                success=False,
                file_name=file_name,
                file_type=file_type,
                total_pages=0,
                error_message=f"结果转换失败: {str(e)}"
            )


# 便捷函数

async def parse_file(
    file_path: Optional[Union[str, Path]] = None,
    file_content: Optional[bytes] = None,
    file_name: Optional[str] = None,
    options: Optional[Dict[str, Any]] = None,
    env: str = None
) -> PaddleParseResult:
    """
    便捷函数：解析文档文件
    
    Args:
        file_path: 文件路径
        file_content: 文件内容（字节）
        file_name: 文件名称
        options: 解析选项
        env: 环境名称
        
    Returns:
        PaddleParseResult: 解析结果
    """
    async with PaddleClient(env) as client:
        return await client.parse_file(file_path, file_content, file_name, options)

